package com.xxx.util;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.DocWriteResponse.Result;
import org.elasticsearch.action.admin.indices.alias.Alias;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * ES工具类
 * 参考：https://www.cnblogs.com/ginb/p/8716485.html
 * 
 * 
 */
public class EsUtil {
	
	private static Logger logger = LoggerFactory.getLogger(EsUtil.class);
	
    private static RestHighLevelClient esClient;
	
	public void setEsClient(RestHighLevelClient esClient) {
		EsUtil.esClient = esClient;
	}

	/***************************创建索引**********************************/
	/**
	 * 创建索引
	 */
	public static void createIndex(String indexName, String alias, int shards, int replicas) {
		logger.info("enter method createIndex(). indexName:{}", indexName);
		
		// 1.创建索引
		if(StringUtils.isEmpty(indexName)) { // 如果传入的索引名为空，则使用默认的索引名
			indexName = PropertyUtil.getCtxProperty("elasticsearch.index");
		}
		CreateIndexRequest request = new CreateIndexRequest(indexName.toLowerCase()); // es索引名不允许大写
		//设置索引相关属性
        request.settings(Settings.builder()
                .put("index.number_of_shards", shards) 	// 分片数量（后期不可修改）
                .put("index.number_of_replicas", replicas) // 每个分片的副本数量（后期可修改）
        );
        
        // 2.设置mapping // ES6以上版本不支持mapping操作
//        String source = FileUtil.readFileToStr("/mapping/", "esLive.json");
//        logger.info(source);
//        request.mapping("esLive", source, XContentType.JSON); 
        
        // 3.为索引设置一个别名（好处是真实索引名称发生变化时可以不用修改代码）
        request.alias(new Alias(alias));
        
        // 4.同步执行创建索引操作
        try {
			CreateIndexResponse createIndexResponse = esClient.indices().create(request);
			//指示是否所有节点都已确认请求
			boolean acknowledged = createIndexResponse.isAcknowledged();
			//指示是否在超时之前为索引中的每个分片启动了必需的分片副本数
	        boolean shardsAcknowledged = createIndexResponse.isShardsAcknowledged();
	        logger.info("createIndex | acknowledged:{}, shardsAcknowledged:{}", acknowledged, shardsAcknowledged);
		} catch (IOException e) {
			logger.error("createIndex | IOException:{}", e);
		}
	}

	/***************************添加文档**********************************/
	/**
	 * 添加文档方式一：文档以json字符串形式提供
	 */
	public static void addIndex1(String indexName, String typeName, String jsonStr) {
		try {
			if(StringUtils.isEmpty(indexName)) {
				indexName = PropertyUtil.getCtxProperty("elasticsearch.index");
			}
			IndexRequest indexRequest = new IndexRequest(indexName, typeName, "1");
			indexRequest.source(jsonStr, XContentType.JSON);
			IndexResponse indexResponse = esClient.index(indexRequest);
			Result result = indexResponse.getResult();
			logger.info("addIndex1 | result:{}", result);
		} catch (IOException e) {
			logger.error("addIndex1 | IOException:{}", e);
		}
	}
	
	/**
	 * 添加文档方式二：文档以map形式提供
	 */
	public static void addIndex2(String indexName, String typeName, Map<String, Object> map) {
		try {
			if(StringUtils.isEmpty(indexName)) {
				indexName = PropertyUtil.getCtxProperty("elasticsearch.index");
			}
			IndexRequest indexRequest = new IndexRequest(indexName, typeName);
			indexRequest.source(map);
			IndexResponse indexResponse = esClient.index(indexRequest);
			Result result = indexResponse.getResult();
			logger.info("addIndex2 | result:{}", result);
		} catch (IOException e) {
			logger.error("addIndex2 | IOException:{}", e);
		}
	}
	
	/***************************删除文档**********************************/
	/**
	 * 根据文档ID，删除文档
	 */
	public static void deleteById(String indexName, String typeName, String docId) {
		if(StringUtils.isEmpty(indexName)) {
			indexName = PropertyUtil.getCtxProperty("elasticsearch.index");
		}
		try {
			DeleteRequest delRequest = new DeleteRequest(indexName, typeName, docId);
			DeleteResponse delResponse = esClient.delete(delRequest);
			Result result = delResponse.getResult();
			logger.info("deleteById | result:{}", result);
		} catch (IOException e) {
			logger.error("deleteById | IOException:{}", e);
		}
	}
	
	/***************************更新文档**********************************/
	/**
	 * 更新指定文档的字段值（入参为json格式）
	 */
	public static void updateById(String indexName, String typeName, String docId, String jsonStr) {
		if(StringUtils.isEmpty(indexName)) {
			indexName = PropertyUtil.getCtxProperty("elasticsearch.index");
		}
		try {
			UpdateRequest updateRequest = new UpdateRequest(indexName, typeName, docId);
			updateRequest.doc(jsonStr, XContentType.JSON);
			UpdateResponse updateResponse = esClient.update(updateRequest);
			//返回的UpdateResponse允许检索有关执行操作的信息，如下所示：
	        String index = updateResponse.getIndex();
	        String type = updateResponse.getType();
	        String id = updateResponse.getId();
	        long version = updateResponse.getVersion();
	        logger.info("updateById | index:{}, type:{}, id:{}, version:{}", index, type, id, version);
	        if (updateResponse.getResult() == Result.CREATED) {
	            //处理第一次创建文档的情况（upsert）
	        } else if (updateResponse.getResult() == Result.UPDATED) {
	            //处理文档被更新的情况
	        } else if (updateResponse.getResult() == Result.DELETED) {
	            //处理文档已被删除的情况
	        } else if (updateResponse.getResult() == Result.NOOP) {
	            //处理文档未受更新影响的情况，即文档上未执行任何操作（noop）
	        }
		} catch (IOException e) {
			logger.error("updateById | IOException:{}", e);
		}
	}
	
	/**
	 * 更新文档的字段值（入参为map格式）
	 */
	public static void updateById2(String indexName, String typeName, String docId, Map<String, Object> map) {
		if(StringUtils.isEmpty(indexName)) {
			indexName = PropertyUtil.getCtxProperty("elasticsearch.index");
		}
		try {
			UpdateRequest updateRequest = new UpdateRequest(indexName, typeName, docId);
			updateRequest.doc(map);
			UpdateResponse updateResponse = esClient.update(updateRequest);
			//返回的UpdateResponse允许检索有关执行操作的信息，如下所示：
	        String index = updateResponse.getIndex();
	        String type = updateResponse.getType();
	        String id = updateResponse.getId();
	        long version = updateResponse.getVersion();
	        logger.info("updateById2 | index:{}, type:{}, id:{}, version:{}", index, type, id, version);
	        if (updateResponse.getResult() == Result.CREATED) {
	            //处理第一次创建文档的情况（upsert）
	        } else if (updateResponse.getResult() == Result.UPDATED) {
	            //处理文档被更新的情况
	        } else if (updateResponse.getResult() == Result.DELETED) {
	            //处理文档已被删除的情况
	        } else if (updateResponse.getResult() == Result.NOOP) {
	            //处理文档未受更新影响的情况，即文档上未执行任何操作（noop）
	        }
		} catch (IOException e) {
			logger.error("updateById2 | IOException:{}", e);
		}
	}
	
	/***************************根据ID查询文档**********************************/
	/**
	 * 根据文档id，查询文档
	 * @param indexName 索引名称
	 * @param typeName 文档类型
	 * @param id 文档ID
	 * @param includes 返回结果里包含的字段（如果includes不为空，则excludes不起作用）
	 * @param excludes 返回结果里不包含回的字段
	 * @return
	 */
	public static String getById(String indexName, String typeName, String docId, 
			String[] includes, String[] excludes) {
		if(StringUtils.isEmpty(indexName)) {
			indexName = PropertyUtil.getCtxProperty("elasticsearch.index");
		}
		GetRequest getRequest = new GetRequest(indexName, typeName, docId);
		// true表示从source字段里检索（false表示不从source字段里检索）
		FetchSourceContext fetchSourceContext = new FetchSourceContext(true, includes, excludes);
		getRequest.fetchSourceContext(fetchSourceContext);
		try {
			GetResponse getResponse = esClient.get(getRequest); // get方法
			if(getResponse==null) {
				return null;
			}
			// 结果以json字符串返回
			String resultStr = getResponse.getSourceAsString();
			logger.info("getById | resultStr:{}", resultStr);
			// 结果以map形式返回
			Map<String, Object> resultMap = getResponse.getSourceAsMap();
			logger.info("getById | resultMap:{}", resultMap.toString());
			return resultStr;
		} catch (IOException e) {
			logger.error("getById | IOException:{}", e);
		}
		return null;
	}
	
	/**
	 * 根据文档ID，从stored_field里查询（要求字段在mapping中单独存储）
	 */
	public static String getByStoredField(String indexName, String typeName, String docId, 
			String stroredField) {
		if(StringUtils.isEmpty(indexName)) {
			indexName = PropertyUtil.getCtxProperty("elasticsearch.index");
		}
		try {
			GetRequest getRequest = new GetRequest(indexName, typeName, docId);
			getRequest.storedFields(stroredField); //查询stroredField存储字段（要求将字段分开存储在映射中）
			GetResponse getResponse = esClient.get(getRequest);
			String resultStr = getResponse.getField(stroredField).getValue();
			logger.info("getByStoredField | resultStr:{}", resultStr);
			return resultStr;
		} catch (IOException e) {
			logger.error("getByStoredField | IOException:{}", e);
		}
		return null;
	}
	
	/***************************搜索文档**********************************/
	/**
	 * 搜索文档
	 * @param indexName 索引名
	 * @param typeName 类型名
	 * @param queryBuilder 查询条件
	 * @param tClass class对象
	 * @param from 分页起始索引
	 * @param size 分页大小
	 * @param sortMap 排序规则
	 * @param includesFiled 返回结果包含的字段列表
	 * @param excludesFiled 返回结果不包含的字段列表
	 * @return
	 */
	public static <T> List<T> search(String indexName, String typeName, QueryBuilder queryBuilder, Class<T> tClass,
			Integer from, Integer size, LinkedHashMap<String, SortOrder> sortMap,
			String[] includesFiled, String[] excludesFiled) {
		if(StringUtils.isEmpty(indexName)) {
			indexName = PropertyUtil.getCtxProperty("elasticsearch.index");
		}
		
		// 1.构建SearchSourceBuilder
		SearchSourceBuilder sourceBuilder = buildSearchSourceBuilder(queryBuilder, from, size, 
				sortMap, includesFiled, excludesFiled);
		
		// 2.构建SearchRequest
		SearchRequest searchRequest = new SearchRequest();
		searchRequest.indices(indexName);
		searchRequest.types(typeName);
		searchRequest.source(sourceBuilder);
		
		// 3.执行搜索
		try {
			SearchResponse searchResponse = esClient.search(searchRequest);
			if(searchResponse==null || searchResponse.getHits()==null) {
				return null;
			}
			SearchHits searchHits = searchResponse.getHits();
			long totalHits = searchHits.totalHits; // 总命中数量（搜索结果总数量）
			if(totalHits>0) {
				SearchHit[] hits = searchHits.getHits(); // 搜索结果数据
				// 将结果转换成list
				return Arrays.stream(hits) 	// 将array转换成stream
								.map(e -> JsonUtil.decodeJson(e.getSourceAsString(), tClass)) // map传入一个function
								.collect(Collectors.toList()); // 将结果转换成list返回
			}
		} catch (IOException e) {
			logger.error("search | IOException:{}", e);
		}
		
		return null;
	}

	private static SearchSourceBuilder buildSearchSourceBuilder(QueryBuilder queryBuilder, Integer from, Integer size,
			LinkedHashMap<String, SortOrder> sortMap, String[] includesFiled, String[] excludesFiled) {
		SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
		// 加入queryBuilder
		searchSourceBuilder.query(queryBuilder);
		// 加入from
		if(from!=null && from>0) {
			searchSourceBuilder.from(from);
		}
		// 加入size
		if(size!=null && size>0) {
			searchSourceBuilder.size(size);
		}
		// 加入sortMap
		if(!MapUtils.isEmpty(sortMap)) {
			sortMap.forEach(searchSourceBuilder::sort); 
			// lamda表达式，在sortMap的每个元素上执行searchSourceBuilder的sort操作
			// 表示循环sortMap里的key和value，将他们传到searchSourceBuilder的sort方法里作为参数
		}
		// 加入includesFiled和includesFiled
		searchSourceBuilder.fetchSource(includesFiled, includesFiled);
		// 设置搜索超时时间：60s
		searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
		return searchSourceBuilder;
	}
	
	/**
	 * 高亮搜索文档
	 * @param indexName 索引名
	 * @param typeName 类型名
	 * @param queryBuilder 查询条件
	 * @param tClass class对象
	 * @param from 分页起始索引
	 * @param size 分页大小
	 * @param sortMap 排序规则
	 * @param includesFiled 返回结果包含的字段列表
	 * @param excludesFiled 返回结果不包含的字段列表
	 * @param highlightBuilder 搜索结果高亮展示
	 * @param highlightFields 搜索结果高亮展示字段名
	 * @return
	 */
	public static <T> List<T> searchWithHighlight(String indexName, String typeName, QueryBuilder queryBuilder,
			Class<T> tClass, Integer from, Integer size, LinkedHashMap<String, SortOrder> sortMap,
			String[] includesFiled, String[] excludesFiled, HighlightBuilder highlightBuilder, String... highlightFields) {
		if(StringUtils.isEmpty(indexName)) {
			indexName = PropertyUtil.getCtxProperty("elasticsearch.index");
		}
		
		// 1.构建SearchSourceBuilder
		SearchSourceBuilder sourceBuilder = buildSearchSourceBuilderWithHighlight(queryBuilder, 
				from, size, sortMap, includesFiled, excludesFiled, highlightBuilder, highlightFields);
		
		// 2.构建SearchRequest
		SearchRequest searchRequest = new SearchRequest();
		searchRequest.indices(indexName);
		searchRequest.types(typeName);
		searchRequest.source(sourceBuilder);
		
		// 3.执行搜索
		try {
			SearchResponse searchResponse = esClient.search(searchRequest);
			if(searchResponse==null || searchResponse.getHits()==null) {
				return null;
			}
			SearchHits searchHits = searchResponse.getHits();
			long totalHits = searchHits.totalHits;// 总命中数量（搜索结果总数量）
			if(totalHits>0) {
				SearchHit[] hits = searchHits.getHits(); // 搜索结果数据
				// 将结果转换成list，并进行高亮展示
				return Arrays.stream(hits) // 将结果array转换成stream
							.map(e -> {	   // 然后调用stream的map方法，循环对每个元素进行处理
								try {
									T obj = JsonUtil.decodeJson(e.getSourceAsString(), tClass); // 结果转换成对象
									Map<String, HighlightField> highlights = e.getHighlightFields(); // 高亮字段
									Arrays.stream(highlightFields).forEach(o -> {  // 针对高亮字段进行处理
												HighlightField highlight = highlights.get(o);
												if(highlight!=null) { // 存在高亮字段
													Text[] fragments = highlight.fragments(); // 高亮片段
													String fragmentString = fragments[0].string();
													try {
														// 通过反射机制，将高亮结果set到指定字段highlight中
														Field highlightField = tClass.getDeclaredField("highlight");
														highlightField.setAccessible(true); // 一定要先将private字段属性设置为可访问
														highlightField.set(obj, fragmentString); // 设置字段属性值 
													} catch (Exception e1) {
														e1.printStackTrace();
													}
												}
											}
										);
									return obj;
								} catch (Exception e1) {
									logger.error("searchWithHighlight | IOException:{}", e);
								}
								return null;
							})
							.collect(Collectors.toList()); // 将结果转换成list返回
						
			}
		} catch (IOException e) {
			logger.error("searchWithHighlight | IOException:{}", e);
		}
		
		return null;
	}

	private static SearchSourceBuilder buildSearchSourceBuilderWithHighlight(QueryBuilder queryBuilder, Integer from, Integer size, 
			LinkedHashMap<String, SortOrder> sortMap, String[] includesFiled, String[] excludesFiled,
			HighlightBuilder highlightBuilder, String... highlightFields) {
		SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
		// 加入queryBuilder
		searchSourceBuilder.query(queryBuilder);
		// 加入from
		if(from!=null && from>0) {
			searchSourceBuilder.from(from);
		}
		// 加入size
		if(size!=null && size>0) {
			searchSourceBuilder.size(size);
		}
		// 加入sortMap
		if(!MapUtils.isEmpty(sortMap)) {
			sortMap.forEach(searchSourceBuilder::sort); 
			// lamda表达式，在sortMap的每个元素上执行searchSourceBuilder的sort操作
			// 表示循环sortMap里的key和value，将他们传到searchSourceBuilder的sort方法里作为参数
		}
		// 加入includesFiled和includesFiled
		searchSourceBuilder.fetchSource(includesFiled, includesFiled);
		// 加入highlightBuilder
		Arrays.stream(highlightFields).forEach(highlightBuilder :: field); // 将需要高亮展示的字段通过field方法set到highlightBuilder里
		searchSourceBuilder.highlighter(highlightBuilder);
		// 设置搜索超时时间：60s
		searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
		return searchSourceBuilder;
	}
	
}
